package com.naiterui.ehp.bp.security.config;

import com.naiterui.ehp.bp.security.annotation.AnonymousAccess;
import com.naiterui.ehp.bp.security.jwt.JwtAccessDeniedHandler;
import com.naiterui.ehp.bp.security.jwt.JwtAuthenticationEntryPoint;
import com.naiterui.ehp.bp.security.jwt.TokenConfigurer;
import com.naiterui.ehp.bp.security.jwt.TokenProvider;
import com.naiterui.ehp.bp.security.properties.SecurityProperties;
import com.naiterui.ehp.bp.security.service.IOnlineUserService;
import com.naiterui.ehp.bp.security.service.UserCacheClean;
import com.naiterui.ehp.bp.security.utils.enums.RequestMethodEnum;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  private final TokenProvider tokenProvider;
  private final UserCacheClean userCacheClean;
  private final IOnlineUserService onlineUserService;
  private final SecurityProperties securityProperties;
  private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
  @Autowired
  @Qualifier("requestMappingHandlerMapping")
  private RequestMappingHandlerMapping requestMappingHandlerMapping;
  private final JwtAuthenticationEntryPoint authenticationErrorHandler;

  @Bean
  GrantedAuthorityDefaults grantedAuthorityDefaults() {
    // 去除 ROLE_ 前缀
    return new GrantedAuthorityDefaults("");
  }

  @Bean
  @ConditionalOnMissingBean
  public PasswordEncoder passwordEncoder() {
    // 密码加密方式
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    // 搜寻匿名标记 url： @AnonymousAccess
    Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = this.requestMappingHandlerMapping.getHandlerMethods();
    // 获取匿名标记
    Map<String, Set<String>> anonymousUrls = this.getAnonymousUrl(handlerMethodMap);
    log.info("@AnonymousAccess：{}", anonymousUrls);
    httpSecurity
        // 禁用 CSRF
        .csrf().disable()
//        .addFilterBefore(this.corsFilter, UsernamePasswordAuthenticationFilter.class)
        // 授权异常
        .exceptionHandling()
        .authenticationEntryPoint(this.authenticationErrorHandler)
        .accessDeniedHandler(this.jwtAccessDeniedHandler)
        // 防止iframe 造成跨域
        .and()
        .headers()
        .frameOptions()
        .disable()
        // 不创建会话
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        // 静态资源等等
        .antMatchers(
            HttpMethod.GET,
            "/*.html",
            "/**/*.html",
            "/**/*.css",
            "/**/*.js",
            "/webSocket/**"
        ).permitAll()
        // swagger 文档
        .antMatchers("/swagger-ui.html").permitAll()
        .antMatchers("/swagger-resources/**").permitAll()
        .antMatchers("/webjars/**").permitAll()
        .antMatchers("/*/api-docs").permitAll()
        // 放行OPTIONS请求
        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
        // 自定义匿名访问所有url放行：允许匿名和带Token访问，细腻化到每个 Request 类型
        // GET
        .antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
        // POST
        .antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
        // PUT
        .antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
        // PATCH
        .antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
        // DELETE
        .antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0]))
        .permitAll()
        // 所有类型的接口都放行
        .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0]))
        .permitAll()
        // 所有请求都需要认证
        .anyRequest().authenticated()
        .and().apply(this.securityConfigurerAdapter());
  }

  private Map<String, Set<String>> getAnonymousUrl(
      Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
    Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
    Set<String> get = new HashSet<>();
    Set<String> post = new HashSet<>();
    Set<String> put = new HashSet<>();
    Set<String> patch = new HashSet<>();
    Set<String> delete = new HashSet<>();
    Set<String> all = new HashSet<>();
    for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
      HandlerMethod handlerMethod = infoEntry.getValue();
      AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
      if (null != anonymousAccess) {
        List<RequestMethod> requestMethods = new ArrayList<>(
            infoEntry.getKey().getMethodsCondition().getMethods());
        RequestMethodEnum request = RequestMethodEnum.find(
            requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType()
                : requestMethods.get(0).name());
        switch (Objects.requireNonNull(request)) {
          case GET:
            get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case POST:
            post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case PUT:
            put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case PATCH:
            patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case DELETE:
            delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          default:
            all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
        }
      }
    }
    anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
    anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
    anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
    anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
    anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
    anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
    return anonymousUrls;
  }

  @Override
  public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/actuator/health");
  }

  private TokenConfigurer securityConfigurerAdapter() {
    return new TokenConfigurer(this.tokenProvider, this.securityProperties, this.onlineUserService, this.userCacheClean);
  }
}
